package bsh.dyn;

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.CompilationState;
import bsh.CompiledScript;
import bsh.ReflectionUtils;
import bsh.compilation.ReflectionUtilsImpl;
import bsh.exception.CompilationFailedException;
import bsh.meta.JavaClassWrapper;
import bsh.meta.MethodDescriptor;
import bsh.meta.TypeWrapper;
import bsh.utils.reflection.transform.PrimitiveWideningTypeTransformations;

/**
 * Resolves dynamic invocations
 * 
 * @author takeshi
 */
public final class IndyResolver {

    /**
     * 
     */
    private IndyResolver() {
    }

    public static final int INFINITE_DISTANCE = Integer.MAX_VALUE;

    /**
     * Comparator that sorts {@link AdaptedInvocable}'s by their {@link ArgumentsAdaptor#weight()}'s
     */
    private static final Comparator<AdaptedInvocable> WEIGHT_COMPARATOR = new Comparator<AdaptedInvocable>() {

        public int compare(AdaptedInvocable o1, AdaptedInvocable o2) {
            return o1.adaptor().weight() - o2.adaptor().weight();
        }
    };

    /**
     * Comparator that sorts by specificity
     */
    private static final Comparator<AdaptedInvocable> SPECIFIC_INVOCABLE_COMPARATOR = new Comparator<AdaptedInvocable>() {

        public int compare(AdaptedInvocable o1, AdaptedInvocable o2) {
            if (this.areEquals(o1, o2)) {
                return 0;

            } else if (this.isMoreSpecificThan(o1, o2)) {
                return -1;
            } else {
                return 1;
            }
        }

        private boolean isMoreSpecificThan(AdaptedInvocable o1, AdaptedInvocable o2) {
            if (o1.isVarargs()) {
                return o2.isVarargs() && this.areMoreSpecific(o1.getParameterTypes(), o2.getParameterTypes());
            } else if (o2.isVarargs()) {
                return true;
            } else {
                return this.areMoreSpecific(o1.getParameterTypes(), o2.getParameterTypes());
            }
        }

        private boolean areMoreSpecific(DeclaredParameterType[] parameterTypes, DeclaredParameterType[] parameterTypes2) {
            if (parameterTypes.length != parameterTypes2.length) {
                throw new IllegalArgumentException("cannot compare arrays with different sizes");
            }
            for (int i = 0; i < parameterTypes.length; i++) {
                if (!this.isMoreSpecific(parameterTypes[i], parameterTypes2[i])) {
                    return false;
                }
            }
            return true;
        }

        private boolean isMoreSpecific(DeclaredParameterType declaredParameterType, DeclaredParameterType declaredParameterType2) {
            return declaredParameterType2.isSuperClassOf(declaredParameterType);
        }

        private boolean areEquals(AdaptedInvocable o1, AdaptedInvocable o2) {
            return (o1.innerInvocable().isVarargs() == o2.innerInvocable().isVarargs())
                    && Arrays.equals(o1.getParameterTypes(), o2.getParameterTypes());
        }
    };

    /**
     * Finds the best method that fits the name (exactly), invocation target and parameters. May throw an {@link IllegalArgumentException}
     * in case there are ambiguities - two (or more) methods that fit and have the same specificity - just like the JLS
     * 
     * @param name the method name
     * @param target the {@link InvocationTarget}
     * @param parameters the array of parameters
     * @return {@link Invocable} or <code>null</code> if none can be found
     */
    public static Invocable findBestMethod(String name, InvocationTarget target, Object[] parameters) {
        return findBestMethod(name, target, toActualParameters(parameters));
    }

    /**
     * Finds the best constructor that fits the declaring type and paremeters. May throw an Exception in case there are ambiguities - two
     * (or more) methods that fit and have the same specificity - just like the JLS
     * 
     * @param target the {@link InvocationTarget}
     * @param parameters array of parameters
     * @return {@link Invocable} or <code>null</code> if none can be found
     */
    public static Invocable findBestConstructor(InvocationTarget target, Object[] parameters) {
        return findBestConstructor(target, toActualParameters(parameters));
    }

    /**
     * Finds the best constructor that fits the declaring type and paremeters. May throw an Exception in case there are ambiguities - two
     * (or more) methods that fit and have the same specificity - just like the JLS * @param target
     * 
     * @param target the target
     * @param parameters the array of parameters
     * @return {@link Invocable} or <code>null</code> if none can be found
     */
    public static Invocable findBestConstructor(InvocationTarget target, ActualParameter[] parameters) {
        return selectBestInvocable(adaptInvocables(target.getConstructors(), parameters));
    }

    /**
     * Converts an array of {@link Object} to an array of {@link ActualParameter}.
     * 
     * @param parameters the parameters. May not be null, otherwise a {@link NullPointerException} is thrown
     * @return {@link ActualParameter}
     */
    private static ActualParameter[] toActualParameters(Object[] parameters) {
        ActualParameter[] params = new ActualParameter[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            Object object = parameters[i];
            params[i] = toActualParameter(object);
        }
        return params;
    }

    /**
     * Transforms an {@link Object} into an {@link ActualParameter}
     * 
     * @param object object. May be null for a null parameter
     * @return {@link ActualParameter}
     */
    private static ActualParameter toActualParameter(Object object) {
        if (object == null || object instanceof Void) {
            return NullParameter.INSTANCE;
        } else {
            return new ReferenceParameter(object.getClass());
        }
    }

    /**
     * Finds the best method that fits the name (exactly), invocation target and parameters. May throw an {@link IllegalArgumentException}
     * in case there are ambiguities - two (or more) methods that fit and have the same specificity - just like the JLS
     * 
     * @param name the method name
     * @param target the {@link InvocationTarget}
     * @param parameters the array of parameters
     * @return {@link Invocable} or <code>null</code> if none can be found
     */
    public static Invocable findBestMethod(String name, InvocationTarget target, ActualParameter[] parameters) {
        return selectBestInvocable(adaptInvocables(target.methodsWithName(name), parameters));
    }

    /**
     * Selects the best invocable among a list of {@link AdaptedInvocable}'s, where best means the one with the smaller weight and more
     * specific, according to the JLS. Throws an {@link IllegalArgumentException} if two or more methods are considered of equal weight and
     * specificity.
     * 
     * @param adaptedInvocables the {@link List}
     * @return {@link Invocable} or <code>null</code> if none can be found
     */
    private static Invocable selectBestInvocable(List<AdaptedInvocable> adaptedInvocables) {
        Collections.sort(adaptedInvocables, WEIGHT_COMPARATOR);
        List<AdaptedInvocable> adapted = adaptedInvocables;
        if (adapted.isEmpty() || adapted.get(0).adaptor().weight() == INFINITE_DISTANCE) {
            return null;
        }
        if (adapted.size() == 1) {
            return adapted.get(0);
        }
        AdaptedInvocable best = adapted.get(0);
        adapted = filter(adapted, best.adaptor().weight());
        Collections.sort(adapted, SPECIFIC_INVOCABLE_COMPARATOR);
        if (adapted.size() > 1 && SPECIFIC_INVOCABLE_COMPARATOR.compare(adapted.get(0), adapted.get(1)) == 0) {
            throw new IllegalArgumentException("Ambiguous call");
        }
        best = adapted.get(0);
        return best;
    }

    /**
     * Filters out elements which weight is not the same as the given weight. The original list is modifieed.
     * 
     * @param adapted the list to filter
     * @param weight the desired weight
     * @return {@link List}
     */
    private static List<AdaptedInvocable> filter(List<AdaptedInvocable> adapted, int weight) {
        for (Iterator<AdaptedInvocable> it = adapted.iterator(); it.hasNext();) {
            if (it.next().adaptor().weight() != weight) {
                it.remove();
            }
        }
        return adapted;
    }

    /**
     * Adapts each invocable (if possible) in order for them to be able to be called by the given parameters. The order of the given
     * collection is preserved
     * 
     * @param invocables the list of invocables
     * @param parameters the array of actual parameters
     * @return a list of {@link AdaptedInvocable}
     */
    private static List<AdaptedInvocable> adaptInvocables(Collection<? extends Invocable> invocables, ActualParameter[] parameters) {
        List<AdaptedInvocable> result = new ArrayList<AdaptedInvocable>(invocables.size());
        for (Invocable inv : invocables) {
            AdaptedInvocable adapt = adapt(inv, parameters);
            if (adapt != null && adapt.adaptor().weight() != INFINITE_DISTANCE) {
                result.add(adapt);
            }
        }
        return result;
    }

    /**
     * Adapts an invocable to a list of parameters. May return <code>null</code> if it's impossible to do so
     * 
     * @param inv the invocable to adapt
     * @param parameters the array of actual parameters
     * @return an {@link AdaptedInvocable} or <code>null</code> if it cannot be done
     */
    private static AdaptedInvocable adapt(Invocable inv, ActualParameter[] parameters) {
        if (inv.isVarargs()) {
            return adaptWithVarArgs(inv, parameters);
        }
        if (parameters.length != inv.getParameterTypes().length) {
            return null;
        }
        return adaptExact(inv, parameters, inv.getParameterTypes().length);
    }

    /**
     * Adapt the invocable with the given number of parameters.
     * 
     * @param inv the invocable
     * @param parameters the parameters
     * @param length the exact length
     * @return {@link AdaptedInvocable}
     */
    private static AdaptedInvocable adaptExact(Invocable inv, ActualParameter[] parameters, int length) {
        return new AdaptedInvocable(createAdaptorFor(inv, parameters, length), inv);
    }

    /**
     * Creates an adaptor to adapt the first N method parameters.
     * 
     * @param inv the {@link Invocable} to create an adaptor for
     * @param parameters the list of parameters
     * @param length the number of parameters to adapt
     * @return {@link ArgumentsAdaptorImpl}
     */
    protected static ArgumentsAdaptorImpl createAdaptorFor(Invocable inv, ActualParameter[] parameters, int length) {
        DeclaredParameterType[] parameterTypes = inv.getParameterTypes();
        TransformationList[] txList = new TransformationList[length];
        for (int i = 0; i < length; i++) {
            DeclaredParameterType types = parameterTypes[i];
            txList[i] = findTransformationChain(parameters[i], types);
        }
        return new ArgumentsAdaptorImpl(extractTypes(parameterTypes, length), txList);
    }

    /**
     * Extract the java classes from the declaredParameterTypes.
     * 
     * @param parameterTypes the array of parameters
     * @param length the length
     * @return array of {@link Class}
     */
    private static Class<?>[] extractTypes(DeclaredParameterType[] parameterTypes, int length) {
        Class<?>[] types = new Class<?>[length];
        for (int i = 0; i < length; i++) {
            types[i] = parameterTypes[i].getJavaType();
        }
        return types;
    }

    /**
     * Adapts an {@link Invocable} witch is a varargs.
     * 
     * @param inv the {@link Invocable}
     * @param parameters the parameters
     * @return {@link AdaptedInvocable}
     */
    private static AdaptedInvocable adaptWithVarArgs(Invocable inv, ActualParameter[] parameters) {
        DeclaredParameterType[] parameterTypes = inv.getParameterTypes();
        // first, we will try to check the exact match
        int varArgsParameterIndex = parameterTypes.length - 1;
        if (parameters.length < varArgsParameterIndex) {
            return null;
        }
        if (parameterTypes.length == parameters.length
                && parameterTypes[parameterTypes.length - 1].isSuperClassOf(parameters[parameters.length - 1])) {
            // is already correct. The number ant types of the arguments are the same
            return new AdaptedInvocable(createAdaptorFor(inv, parameters, varArgsParameterIndex + 1), inv);
        }
        Class<?> varArgsParameterType = parameterTypes[parameterTypes.length - 1].getJavaType();
        ArgumentsAdaptorImpl regularParametersAdaptor = createAdaptorFor(inv, parameters, varArgsParameterIndex);
        TransformationList[] transformations =
                collectFrom(parameterTypes[varArgsParameterIndex].getComponentType(), parameters, varArgsParameterIndex);
        return new AdaptedInvocable(new VarArgsArgumentAdaptor(regularParametersAdaptor, varArgsParameterType, transformations), inv);
    }

    /**
     * Adapts the remaining arguments to the varargs component type.
     * 
     * @param parameterType the varargs component type
     * @param parameters all the parameters. The adapted parameters will have index >= varArgsIndex
     * @param varArgsIndex the index of the position of the varargs
     * @return array of {@link TransformationList}
     */
    private static TransformationList[] collectFrom(DeclaredParameterType parameterType, ActualParameter[] parameters, int varArgsIndex) {
        TransformationList[] list = new TransformationList[Math.max(0, parameters.length - varArgsIndex)];
        for (int i = varArgsIndex; i < parameters.length; i++) {
            list[i - varArgsIndex] = findTransformationChain(parameters[i], parameterType);
        }
        return list;
    }

    /**
     * Tries to transform an argument of type from to a parameter of type to.
     * 
     * @param from the {@link ActualParameter}
     * @param to the {@link DeclaredParameterType}
     * @return a {@link TransformationList} or an {@link ImpossibleTransformation}
     */
    public static TransformationList findTransformationChain(ActualParameter from, DeclaredParameterType to) {
        Set<Object> visited = new HashSet<Object>();
        Queue<TransformationList> transformations = new LinkedList<TransformationList>();
        transformations.add(new EmptyTransformationList(new IdentityTransformation(from)));
        while (!transformations.isEmpty()) {
            TransformationList current = transformations.poll();
            ActualParameter target = current.getTransformationTarget();
            if (matches(target, to)) {
                return current;
            }
            for (Transformation tx : findApplicableTransformations(target)) {
                if (visited.add(tx)) {
                    transformations.add(current.appendTransformation(tx));
                }
            }
        }
        return ImpossibleTransformation.IMPOSSIBLE;
    }

    /**
     * Checks if an actual parameter types matches a declared parameter type
     * 
     * @param transformationTarget {@link ActualParameter}
     * @param to {@link DeclaredParameterType}
     * @return boolean
     */
    private static boolean matches(ActualParameter transformationTarget, DeclaredParameterType to) {
        if (transformationTarget.isNull()) {
            return to.acceptsNull();
        }
        return to.isSuperClassOf(transformationTarget);
    }

    /**
     * Finds a list of Transformations that can be applied to an {@link ActualParameter}
     * 
     * @param parameterType the {@link ActualParameter}
     * @return the {@link Collection} of transformations that can be applied
     */
    public static Collection<Transformation> findApplicableTransformations(ActualParameter parameterType) {
        if (parameterType.isNull()) {
            // no transformations
            return Collections.emptyList();
        } else if (parameterType.isPrimitive()) {
            List<PrimitiveWideningTypeTransformations> applicable =
                    PrimitiveWideningTypeTransformations.allTransformationsFrom(parameterType.getJavaType());
            List<Transformation> list = new ArrayList<Transformation>(applicable.size() + 1);
            for (PrimitiveWideningTypeTransformations prim : applicable) {
                list.add(new PrimitiveWideningTransformation(prim));
            }
            if (parameterType.getJavaType().equals(double.class)) {
                list.add(new CompilableTransformation() {

                    public Object transform(Object object, CompiledScript script) {
                        return new BigDecimal(object.toString(), script == null ? MathContext.DECIMAL128 : script.getMathContext());
                    }

                    public boolean isCompilable() {
                        return true;
                    }

                    public int getWeight() {
                        return 1;
                    }

                    public ActualParameter getTransformationTarget() {
                        return new ReferenceParameter(BigDecimal.class);
                    }

                    public boolean acceptsType(ActualParameter parameter) {
                        return parameter.isPrimitive() && parameter.getJavaType().equals(double.class);
                    }

                    public void compile(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
                        state.assertStackTopType(JavaClassWrapper.DOUBLE_WRAPPER);
                        STRING_VALUE_OF.writeInvoke(visitor, state);
                        visitor.visitVarInsn(Opcodes.ALOAD, SCRIPT_HELPER_INDEX);
                        state.pushStack(CompiledScript.class);
                        SCRIPT_GET_MATH_CONTEXT.writeInvoke(visitor, state);
                        state.assertStackTopType(JavaClassWrapper.wrapperFor(BigDecimal.class));
                    }
                });
            }
            return list;
        } else {
            List<Transformation> list = new ArrayList<Transformation>();
            if (parameterType.getSuperType() != null) {
                list.add(new IdentityTransformation(parameterType.getSuperType()));
            }
            for (ActualParameter param : parameterType.getDeclaredInterfaces()) {
                list.add(new IdentityTransformation(param));
            }
            if (parameterType.isPrimitiveWrapper()) {
                list.add(new UnboxTransformation(parameterType));
            }
            return list;
        }
    }

    private static final MethodDescriptor STRING_VALUE_OF, SCRIPT_GET_MATH_CONTEXT;
    static {
        ReflectionUtils utils = new ReflectionUtilsImpl();
        STRING_VALUE_OF =
                utils.findMethodForTypes(JavaClassWrapper.STRING_WRAPPER, "valueOf", new TypeWrapper[]{ JavaClassWrapper.DOUBLE_WRAPPER });
        SCRIPT_GET_MATH_CONTEXT =
                utils.findMethodForTypes(JavaClassWrapper.wrapperFor(CompiledScript.class), "getMathContext", new TypeWrapper[0]);
    }

    /**
     * {@link TransformationList} that is impossible - a path that has infinite distance.
     * 
     * @author takeshi
     */
    private static enum ImpossibleTransformation implements TransformationList {

        IMPOSSIBLE {

            /**
             * {@inheritDoc}
             */
            public ActualParameter getTransformationTarget() {
                return null;
            }

            /**
             * {@inheritDoc}
             */
            public boolean acceptsType(ActualParameter parameter) {
                return false;
            }

            /**
             * {@inheritDoc}
             */
            public int getWeight() {
                return INFINITE_DISTANCE;
            }

            /**
             * {@inheritDoc}
             */
            public TransformationList appendTransformation(Transformation transformation) {
                return this;
            }

            /**
             * {@inheritDoc}
             */
            public Object transform(Object object, CompiledScript script) {
                throw new IllegalArgumentException();
            }

            /**
             * {@inheritDoc}
             */
            public boolean isCompilable() {
                return false;
            }

            /**
             * {@inheritDoc}
             */
            public void compile(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
                throw new IllegalArgumentException("not compilable!");
            }
        }

    }

    /**
     * Just like {@link #findBestMethod(String, InvocationTarget, Object[])}, except that only looks for static methods
     * 
     * @param methodName the method name
     * @param invocationTargetImpl the declaring type (of the static method)
     * @param args the arguments passed to the method
     * @return {@link Invocable}
     */
    public static Invocable findBestStaticMethod(String methodName, InvocationTargetImpl invocationTargetImpl, Object[] args) {
        return selectBestInvocable(adaptInvocables(invocationTargetImpl.staticMethodsWithName(methodName), toActualParameters(args)));
    }

}
